package org.bdware.doip.core.codec;

import com.google.gson.*;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import org.apache.log4j.Logger;
import org.bdware.doip.core.doipMessage.*;
import org.bdware.doip.core.exception.DoDecodeException;
import org.bdware.doip.core.model.digitalObject.DigitalObject;
import org.bdware.doip.core.utils.DoipGson;
import org.bdware.doip.core.utils.GlobalConfigurations;

import java.io.EOFException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Random;

public class DelimiterMessageCodecImpl implements DelimiterMessageCodec{
    Logger logger = Logger.getLogger(DelimiterMessageCodecImpl.class);
    private final byte[] SEGMENT_TERMINATOR = Delimiters.SEGMENT_TERMINATOR;
    private final byte[] EMPTY_SEGMENT = Delimiters.EMPTY_SEGMENT;
    DigitalObjectCodec codec = new DigitalObjectCodecImpl();
    Random rdm = new Random();
    private final HashMap<Integer,String> requestIDMap = new HashMap<>();

    @Override
    public byte[] ResponseToBytes(DoipMessage response) {
        ByteBuf buff = Unpooled.directBuffer();
        DelimiterResponse resp = new DelimiterResponse();
        if(requestIDMap.get(response.requestID) == null)
            resp.requestId = response.requestID+"";
        else{
            resp.requestId = requestIDMap.get(response.requestID);
            requestIDMap.remove(response.requestID);
        }
        resp.status = response.header.parameters.response.getName();
        logger.debug("response body: " + new String(response.body.getEncodedData()));
        try {
            DigitalObject outDO = response.body.getDataAsDigitalObject();
            if(outDO == null) throw new DoDecodeException("decode DO error");
            String jsonSegments = DoipGson.getDoipGson().toJson(resp);
            logger.debug("response json segs: " + jsonSegments);
            buff.writeBytes(jsonSegments.getBytes());
            buff.writeBytes(SEGMENT_TERMINATOR);
            ArrayList<byte[]> doSegments = codec.DoToSegments(outDO);
            for(byte[]seg:doSegments){
                buff.writeBytes(seg);
                buff.writeBytes(SEGMENT_TERMINATOR);
            }
        } catch (DoDecodeException de) {
            logger.debug("not do segment: " + new String(response.body.encodedData));
            String elementDataSeg = null;
            if(response.header.parameters!= null && response.header.parameters.attributes != null && response.header.parameters.attributes.has("element")){
                elementDataSeg = ("@\n"+ response.body.getLength() +"\n" + new String(response.body.getEncodedData()));
            }else if(response.body.getLength() > 0){
                try {
                    resp.output = JsonParser.parseString(new String(response.body.encodedData)).getAsJsonObject();
                }catch (Exception parseJOError){
                    try{
                        resp.output = JsonParser.parseString(new String(response.body.encodedData)).getAsJsonArray();
                    }catch (Exception parseJAError){
                        JsonObject tempJo = new JsonObject();
                        tempJo.addProperty("message", new String(response.body.encodedData));
                        resp.output = tempJo;
                    }
                }
            }
            String jsonSegments = new Gson().toJson(resp);
            logger.debug("response: " + jsonSegments);
            buff.writeBytes(jsonSegments.getBytes());
            buff.writeBytes(SEGMENT_TERMINATOR);
            if(elementDataSeg != null){
                buff.writeBytes(elementDataSeg.getBytes());
                buff.writeBytes(SEGMENT_TERMINATOR);
            }
        }catch (EOFException eofException){
            String jsonSegments = new Gson().toJson(resp);
            logger.debug("response: " + jsonSegments);
            buff.writeBytes(jsonSegments.getBytes());
            buff.writeBytes(SEGMENT_TERMINATOR);
        }catch (Exception e){
            e.printStackTrace();
        }
        buff.writeBytes(EMPTY_SEGMENT);
        byte[] msgBytes = new byte[buff.readableBytes()];
        buff.readBytes(msgBytes);
        logger.debug("server sending message: "+ new String(msgBytes));
        return msgBytes;
    }

    @Override
    public DoipMessage BytesToResponse(byte[] responseBytes){
        logger.debug("decode response message: "+ new String(responseBytes));
        DelimiterResponse resp;
        String[] msgSegments = getSegments(responseBytes);
        assert msgSegments.length > 0;

        resp = new Gson().fromJson(msgSegments[0], DelimiterResponse.class);
        DoipMessage inMsg = new DoipMessage(null,null);
        inMsg.header.IsRequest = false;
        inMsg.requestID = Integer.parseInt(resp.requestId);
        inMsg.header.parameters.response = DoipResponseCode.getDoResponse(resp.status);

        if(resp.output != null){
            String outputStr = new Gson().toJson(resp.output);
            int a = outputStr.getBytes().length;
            ByteBuf bodyReader = Unpooled.buffer();
            bodyReader.writeInt(a);
            bodyReader.writeBytes(outputStr.getBytes());
            inMsg.body.encodedData = new byte[bodyReader.readableBytes()];
            bodyReader.readBytes(inMsg.body.encodedData);
        }else{
            if(msgSegments.length > 1){
                ArrayList<byte[]> doSegs = new ArrayList<>();
                for(int i=1;i<msgSegments.length;i++){
                    doSegs.add(msgSegments[i].getBytes());
                }
                try{
                    DigitalObject inDO = codec.SegmentsToDo(doSegs);
                    inMsg.body.setDataAsDigitalObject(inDO);
                }catch (DoDecodeException e){
                    if (doSegs.size() == 1) {
                        inMsg.body.encodedData = doSegs.get(0);
                    }else{
                        logger.warn("unexpected segments size" + doSegs.size());
                    }
                }
            }else{
                logger.debug("empty output");
            }
        }
        inMsg.header.bodyLength = inMsg.body == null? 0:inMsg.body.getLength();
        return inMsg;
    }

    @Override
    public byte[] RequestToBytes(DoipMessage request) throws IOException, DoDecodeException {
        ByteBuf buff = Unpooled.directBuffer();
        DelimiterRequest req = new DelimiterRequest();
        req.requestId = request.requestID+"";
        req.operationId = request.header.parameters.operation;
        req.targetId = request.header.parameters.id;
//        req.clientId = GlobalConfigurations.User_Handle;
        req.attributes = request.header.parameters.attributes;
        if(GlobalConfigurations.CordraUsername != null && GlobalConfigurations.CordraPassword != null){
            if (req.authentication == null) {
                req.authentication = new JsonObject();
            }
            req.authentication.addProperty("username",GlobalConfigurations.CordraUsername);
            req.authentication.addProperty("password",GlobalConfigurations.CordraPassword);
        }
        String jsonSegStr = new Gson().toJson(req);
        buff.writeBytes(jsonSegStr.getBytes());
        buff.writeBytes(SEGMENT_TERMINATOR);
        if(request.body != null && request.body.getLength()>0){
            if(request.header.parameters.attributes!= null && request.header.parameters.attributes.has("elementID")){
                buff.writeBytes(request.body.encodedData);
                buff.writeBytes(SEGMENT_TERMINATOR);
            }else{
                DigitalObject inDO = request.body.getDataAsDigitalObject();
                for(byte[] seg:codec.DoToSegments(inDO)){
                    buff.writeBytes(seg);
                    buff.writeBytes(SEGMENT_TERMINATOR);
                }
            }
        }
        buff.writeBytes(EMPTY_SEGMENT);
        ByteBuf outMsg = buff.copy();
        byte[] msgBytes = new byte[outMsg.readableBytes()];
        outMsg.readBytes(msgBytes);
        return msgBytes;
    }

    @Override
    public DoipMessage BytesToRequest(byte[] requestBytes) {
        DelimiterRequest req;

        logger.debug("decode request: "+ new String(requestBytes));

        String[] segStrings = getSegments(requestBytes);
        assert segStrings.length > 0;

        req = new Gson().fromJson(segStrings[0],DelimiterRequest.class);
        int requestID;
        try{
            requestID = Integer.parseInt(req.requestId);
        }catch (NumberFormatException e){
            requestID = rdm.nextInt();
            while(requestIDMap.get(requestID) != null){
                requestID = rdm.nextInt();
            }
            requestIDMap.put(requestID,req.requestId);
        }

        DoipMessage inMsg  = new DoipMessage(req.targetId,req.operationId);
        inMsg.header.IsRequest = true;
        inMsg.requestID = requestID;

        if(req.attributes != null)
            for(String key : req.attributes.keySet()){
                inMsg.header.parameters.addAttribute(key,req.attributes.get(key).getAsString());
            }

        if(req.input!= null && req.input.keySet().size()>0){
            if(segStrings.length>1){
                logger.debug("unexpected segments.");
                inMsg.header.parameters.response = DoipResponseCode.Invalid;
                return inMsg;
            }
            String jsonInput = new Gson().toJson(req.input);
            int a = jsonInput.getBytes().length;
            ByteBuf bodyReader = Unpooled.buffer();
            bodyReader.writeInt(a);
            bodyReader.writeBytes(jsonInput.getBytes());
            DigitalObject tempDO = DoipGson.getDoipGson().fromJson(jsonInput,DigitalObject.class);
            inMsg.body.setDataAsDigitalObject(tempDO);
        } else{
            if(segStrings.length<2){
                logger.debug("no input data segments");
                return inMsg;
            }
            ArrayList<byte[]> doSegments = new ArrayList<>();
            for(int i=1;i<segStrings.length;i++){
                doSegments.add(segStrings[i].getBytes());
            }
            DigitalObject inputDO = null;
            try {
                inputDO = codec.SegmentsToDo(doSegments);
                inMsg.body.setDataAsDigitalObject(inputDO);
            } catch (DoDecodeException e) {
                //body是element
                inMsg.body.encodedData = doSegments.get(0);
            } catch (Exception e){
                e.printStackTrace();
            }
            inMsg.body.setDataAsDigitalObject(inputDO);
        }
        return inMsg;
    }

    private String[] getSegments(byte[] msgBytes){
        return new String(msgBytes).split(new String(SEGMENT_TERMINATOR));
    }
}
